package engine

import (
	"context"
	"log"
	"os"
	"path/filepath"
	"time"

	"vov.cx/kagura/parser"
	"vov.cx/kagura/parser/mod"
	"vov.cx/kagura/utils/dirwatch"
	"vov.cx/kagura/utils/exec"
	"vov.cx/kagura/utils/gobuild"

	"github.com/fsnotify/fsnotify"
	"github.com/pkg/errors"
	"github.com/segmentio/go-snakecase"
)

type Engine struct {
	srcDIR   string
	buildDIR string
	cur      *exec.Command
	builder  *gobuild.Builder
}

func New(srcDIR string, builder *gobuild.Builder) (*Engine, error) {
	srcMod, err := mod.Open(filepath.Join(srcDIR, "go.mod"))
	if err != nil {
		return nil, err
	}

	if srcMod.Module == nil || srcMod.Module.Mod.Path == "" {
		return nil, errors.Errorf("no mod path for: %s", srcDIR)
	}

	buildDIR := filepath.Join(os.TempDir(), snakecase.Snakecase(srcMod.Module.Mod.Path))
	if builder.Output == "" {
		builder.Output = filepath.Join(buildDIR, filepath.Base(buildDIR))
	} else {
		builder.Output, _ = filepath.Abs(builder.Output)
	}

	return &Engine{srcDIR: srcDIR, buildDIR: buildDIR, builder: builder}, nil
}

// Clean 清理临时目录
func (e *Engine) Clean(ctx context.Context) error {
	return e.clean(ctx)
}

// Build 生成, run 指示是否生成后运行
func (e *Engine) Build(ctx context.Context, run bool) error {
	return properRun(ctx, e.pipes(run)...)
}

// Watch 生成并监听工作目录
func (e *Engine) Watch(ctx context.Context, run bool) error {
	w, err := dirwatch.New(e.srcDIR)
	if err != nil {
		return err
	}

	pipes := e.pipes(run)

	doRun := func(clean bool) func() {
		var curPipes = make([]ProperFunc, 0, len(pipes))
		if clean {
			curPipes = append(curPipes, pipes...)
		} else {
			curPipes = append(curPipes, pipes[2:]...)
		}

		return func() {
			if e.cur != nil {
				if err := e.cur.Kill(); err != nil {
					log.Printf("kill error: %v\n", err)
				}
				e.cur = nil
			}

			if err := properRun(ctx, curPipes...); err != nil {
				log.Printf("run error: %v\n", err)
			}
		}
	}

	var cancel context.CancelFunc = func() {}
	w.SetOnChanged(func(ev fsnotify.Event) {
		log.Println(ev.String())
		cancel()
		cancel = delayRun(ctx, doRun(false), time.Second*3)
	})

	go doRun(true)()

	return w.Watch(ctx)
}

func (e *Engine) exec(ctx context.Context, command *exec.Command) error {
	e.cur = command
	return e.cur.Run(ctx)
}

func (e *Engine) clean(context.Context) error {
	if e.buildDIR == "" || e.buildDIR == "/" {
		return errors.New("build dir is not generated")
	}
	if err := os.RemoveAll(e.buildDIR); err != nil {
		return errors.WithStack(err)
	}
	return nil
}

func (e *Engine) prepare(ctx context.Context) error {
	return parser.ExtraApp(ctx, e.buildDIR, nil)
}

func (e *Engine) generate(context.Context) error {
	prj, err := parser.Parse(e.srcDIR)
	if err != nil {
		return err
	}

	if err := e.writeRouter(prj, e.buildDIR); err != nil {
		return err
	}

	return mod.Update(e.buildDIR, func(tx *mod.File) error {
		if err := tx.AddReplace(prj.Module.Package, "", prj.Module.Path, ""); err != nil {
			return errors.WithStack(err)
		}
		if err := tx.AddRequire(prj.Module.Package, "v0.0.0-00010101000000-000000000000"); err != nil {
			return errors.WithStack(err)
		}
		return nil
	})
}

func (e *Engine) modTidy(ctx context.Context) error {
	return e.exec(ctx, exec.Go("mod", "tidy").SetDir(e.buildDIR).OnStateChanged(e.onStateChanged))
}

func (e *Engine) build(ctx context.Context) error {
	return e.exec(ctx, e.builder.BuildCommand("build").SetDir(e.buildDIR).OnStateChanged(e.onStateChanged))
}

func (e *Engine) upx(ctx context.Context) error {
	if e.builder.UpxPack && e.builder.Output != "" {
		return e.exec(ctx, e.builder.Upx().SetDir(e.buildDIR).OnStateChanged(e.onStateChanged))
	}
	return nil
}

func (e *Engine) run(ctx context.Context) error {
	return e.exec(ctx, exec.New(e.builder.Output).SetDir(e.srcDIR).OnStateChanged(e.onStateChanged))
}

func (e *Engine) pipes(run bool) []ProperFunc {
	pipes := []ProperFunc{e.clean, e.prepare, e.generate, e.modTidy, e.build}
	if run {
		pipes = append(pipes, e.run)
	} else {
		pipes = append(pipes, e.upx)
	}
	return pipes
}

func (e *Engine) writeRouter(prj *parser.Project, buildDIR string) error {
	routerFw, err := os.Create(filepath.Join(buildDIR, "router.go"))
	if err != nil {
		return errors.WithStack(err)
	}
	defer routerFw.Close()
	return prj.WriteGo(routerFw)
}

func (e *Engine) isReBuildEvent(ev fsnotify.Event) bool {
	return ev.Op&fsnotify.Rename == fsnotify.Rename ||
		ev.Op&fsnotify.Create == fsnotify.Create ||
		ev.Op&fsnotify.Remove == fsnotify.Remove ||
		ev.Op&fsnotify.Write == fsnotify.Write
}

func (e *Engine) onStateChanged(c *exec.Command) {
	status := c.Status()
	if status.Exited {
		e.cur = nil
	}
}
